package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"litetalk/api/form"
	"litetalk/proto"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
)

const (
	apiAddr      = "http://localhost:8100"
	connAddr     = "localhost:8200"
	registerPath = "/user/register"
	loginPath    = "/user/login"
	allUserPath  = "/user/all"
	onlinePath   = "/user/online"
	contentType  = "application/json"
)

type UserInfo struct {
	User   proto.IMUser
	Online bool
}

type Client struct {
	conn         net.Conn
	authToken    string
	Id           int
	Name         string
	exit         bool
	writeMsgChan chan *proto.Msg
	quit         chan os.Signal
	friends      map[uint]*UserInfo
}

func main() {
	c := new(Client)
	c.writeMsgChan = make(chan *proto.Msg, 10)

	if !c.init() {
		fmt.Println("bye")
		return
	}
	if err := c.connect(); err != nil {
		fmt.Println("connect error: ", err)
		return
	}

	c.quit = make(chan os.Signal, 1)
	signal.Notify(c.quit, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	<-c.quit
	c.Close()
	fmt.Println("bye")
}

func (c *Client) init() bool {
	c.friends = make(map[uint]*UserInfo)
	scanner := bufio.NewScanner(os.Stdin)

ReScan:
	fmt.Println("- register [user_name] [password] [nickname]")
	fmt.Println("- login [user_name] [password]")
	if scanner.Scan() {

		cmd := string(scanner.Bytes())
		cmds := strings.Split(cmd, " ")

		switch cmds[0] {
		case "register":
			if err := register(cmds[1], cmds[2], cmds[3]); err != nil {
				fmt.Println("register error ", err)
			} else {
				fmt.Println("register ok. ")
				goto ReScan
			}
		case "login":
			if err := c.login(cmds[1], cmds[2]); err != nil {
				fmt.Println("login error ", err)
			} else {
				return true
			}
		default:
			fmt.Println("unknown cmd ", cmds[0])
		}
		goto ReScan
	}

	return false
}

func (c *Client) connect() error {
	fmt.Println("dial before to: ", connAddr)
	conn, err := net.Dial("tcp", connAddr)
	if err != nil {
		return err
	}
	c.conn = conn
	c.exit = false
	c.buildConnect()
	c.GetAllUser()
	c.GetOnlineUser()

	fmt.Println("connect to: ", connAddr)
	go c.readRun()
	go c.writeRun()

	return nil
}

func (c *Client) buildConnect() {
	opt := &proto.Connect{
		AuthToken: c.authToken,
	}
	data, _ := json.Marshal(opt)
	pack := proto.NewMsg(proto.OpBuildConn, data, 0)
	pack.Pack(c.conn)
}

func (c *Client) readRun() {
	scanner := proto.NewScanner(c.conn)

	for scanner.Scan() {
		scanPack := new(proto.Msg)
		err := scanPack.Unpack(bytes.NewReader(scanner.Bytes()))
		if err != nil {
			continue
		}

		switch scanPack.Op {
		case proto.OpPing:
			c.writeMsgChan <- proto.NewMsg(proto.OpPong, nil, 0)
		case proto.OpKickUserOnline:
			m := &proto.IMKickUser{}
			json.Unmarshal(scanPack.Msg, m)
			v, ok := c.friends[m.UserId]
			if ok && !v.Online {
				v.Online = true
				c.friends[m.UserId] = v
			} else {
				v = &UserInfo{
					Online: true,
				}
				c.friends[m.UserId] = v
			}
			fmt.Printf("> [%-3d %s] Online\n", v.User.Id, v.User.RealName)
		case proto.OpKickUserOffline:
			m := &proto.IMKickUser{}
			json.Unmarshal(scanPack.Msg, m)
			v, ok := c.friends[m.UserId]
			if ok && v.Online {
				v.Online = false
				c.friends[m.UserId] = v
			} else {
				c.friends[m.UserId] = &UserInfo{
					Online: false,
				}
			}
			fmt.Printf("> [%-3d %s] Offline\n", v.User.Id, v.User.RealName)
		case proto.OpSingleSend:
			m := &proto.IMMessage{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				fmt.Println("ERR: readRun recv msg error")
				continue
			}
			fmt.Printf("> [%d]: %s\n", m.FromUserId, m.MsgData)
		case proto.OpGroupSend:
			m := &proto.IMMessage{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				fmt.Println("ERR: group recevie msg error")
				continue
			}
			fmt.Printf("> GROUP[%d-%d]: %s\n", m.SessionId, m.FromUserId, m.MsgData)
		case proto.OpSingleSendAck:
			m := &proto.IMMessageAck{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				fmt.Printf("ERR: readRun recv msg error\n")
				continue
			}
			fmt.Println("INFO: message send ok!")
		case proto.OpAllUserAck:
			m := &proto.IMAllUserRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				fmt.Println("ERR OpAllUserAck Unmarshal err: ", err)
				continue
			}
			for _, user := range m.UserList {
				c.friends[user.Id] = &UserInfo{
					User:   user,
					Online: false,
				}
			}
		case proto.OpOnlineUserAck:
			m := &proto.IMOnlineUserRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				fmt.Println("ERR OpOnlineUserAck Unmarshal err: ", err)
				continue
			}
			for _, id := range m.UserIdList {
				c.friends[id].Online = true
			}
		case proto.OpGroupCreateAck:
			m := &proto.IMGroupCreateRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				fmt.Println("ERR OpGroupCreateAck Unmarshal err: ", err)
				continue
			}
			if m.Code == 0 {
				fmt.Println("INFO: create group ok id: ", m.GroupId)
			} else {
				fmt.Println("ERRR: create group failed!")
			}
		case proto.OpNormalGroupListAck:
			m := &proto.IMNormalGroupListRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				continue
			}
			if m.Code == 0 {
				fmt.Println("    index    id")

				for i, id := range m.GroupIdList {
					fmt.Printf("     %d        %d\n", i, id)
				}
			} else {
				fmt.Println("NomalGroupList get error")
			}
		case proto.OpGroupInfoListAck:
			m := &proto.IMGroupInfoListRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				continue
			}
			if m.Code == 0 {
				fmt.Println("    index  id   name   member")
				for i, info := range m.GroupInfoList {
					fmt.Printf("     %d      %d    %s     %v\n", i, info.GroupId, info.GroupName, info.MemberId)
				}
			} else {
				fmt.Println("GroupInfoList get error")
			}
		case proto.OpGroupChangeMemberAck:
			m := &proto.IMGroupChangeMemberRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				continue
			}
			if m.Code == 0 {
				fmt.Println("group change ok!")
			} else {
				fmt.Println("group change failed!")
			}
		case proto.OpGroupChangeMemberNotify:
			m := &proto.IMGroupChangeMemberNotify{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				continue
			}
			fmt.Println("Group member change id:", m.GroupId)
		case proto.OpGetLatestMsgIdAck:
			m := &proto.IMGetLastestMsgIdRes{}
			if err := json.Unmarshal(scanPack.Msg, m); err != nil {
				continue
			}
			fmt.Printf("INFO: last msg to %d id is %d\n", m.SessionId, m.LastestMsgId)
		default:
			fmt.Printf("ERR: readRun unknown operator %d\n", scanPack.Op)
		}
	}
	c.exit = true
	close(c.writeMsgChan)
	close(c.quit)
	fmt.Println("exit...")
}

func (c *Client) writeRun() {
	scanner := bufio.NewScanner(os.Stdin)

	go func() {
		client := c
		for !client.exit {
			p, ok := <-c.writeMsgChan
			if ok {
				p.Pack(c.conn)
			}
		}
	}()

	for scanner.Scan() {
		if c.exit {
			break
		}
		cmd := string(scanner.Bytes())
		if len(cmd) == 0 {
			continue
		}
		cmds := strings.Split(cmd, " ")
		if len(cmds) > 0 {
			switch cmds[0] {
			case "send":
				c.sendMsg(cmds[1:])
			case "list":
				c.listUsers(cmds[1:])
			case "create":
				c.create(cmds[1:])
			case "group":
				c.group(cmds[1:])
			case "query":
				c.query(cmds[1:])
			default:
				fmt.Println("unknown cmd ", cmds[0])
			}
		}
	}
}

func (c *Client) sendMsg(cmds []string) {
	if len(cmds) == 0 {
		return
	}
	switch cmds[0] {
	case "user":
		c.sendUserMsg(cmds[1:])
	case "group":
		c.sendGroupMsg(cmds[1:])
	default:
		fmt.Println("unknown msg type ", cmds[0])
	}
}

func (c *Client) sendUserMsg(cmds []string) {
	if len(cmds) < 2 {
		fmt.Println("no target or msg")
		return
	}
	id, err := strconv.Atoi(cmds[0])
	if err != nil {
		fmt.Println("invalid id")
		return
	}

	if id == c.Id {
		fmt.Println("can't send message to self")
		return
	}

	str := strings.Join(cmds[1:], " ")

	sendData := &proto.IMMessage{
		FromUserId: uint(c.Id),
		SessionId:  uint(id),
		MsgData:    str,
	}
	sendBodyBytes, _ := json.Marshal(sendData)
	sendPack := proto.NewMsg(proto.OpSingleSend, sendBodyBytes, 0)
	c.writeMsgChan <- sendPack
}

func (c *Client) sendGroupMsg(cmds []string) {
	if len(cmds) < 2 {
		fmt.Println("ERR: Send group no target or message")
	}
	id, err := strconv.Atoi(cmds[0])
	if err != nil {
		fmt.Println("invalid id")
		return
	}

	str := strings.Join(cmds[1:], "")
	sendData := &proto.IMMessage{
		FromUserId: uint(c.Id),
		SessionId:  uint(id),
		MsgData:    str,
	}
	body, _ := json.Marshal(sendData)
	pack := proto.NewMsg(proto.OpGroupSend, body, 0)
	c.writeMsgChan <- pack
}

func (c *Client) listUsers(cmds []string) {
	fmt.Println("    [id   -- name]")
	for _, v := range c.friends {
		if v.User.Id == uint(c.Id) {
			continue
		}
		if v.Online {
			fmt.Printf("    [%-4d -- %s]\n", v.User.Id, v.User.RealName)
		}
	}
}

func (c *Client) create(cmds []string) {
	if len(cmds) == 0 {
		return
	}

	switch cmds[0] {
	case "group":
		c.createGroup(cmds[1:])
	}
}

func (c *Client) group(cmds []string) {
	if len(cmds) == 0 {
		return
	}

	switch cmds[0] {
	case "list":
		c.groupList(cmds[1:])
	case "info":
		c.groupInfo(cmds[1:])
	case "add":
		c.groupChange(cmds[1:], proto.CID_OK)
	case "sub":
		c.groupChange(cmds[1:], proto.CID_DEL)
	}
}

func (c *Client) query(cmds []string) {
	if len(cmds) == 0 {
		return
	}

	switch cmds[0] {
	case "message":
		c.queryMessage(cmds[1:])
	case "lastmsg":
		c.queryLastMsg(cmds[1:])
	}
}

func (c *Client) groupList(cmds []string) {
	req := &proto.IMNormalGroupListReq{
		UserId: uint(c.Id),
	}

	sendBodyBytes, _ := json.Marshal(req)
	sendPack := proto.NewMsg(proto.OpNormalGroupList, sendBodyBytes, 0)
	c.writeMsgChan <- sendPack
}

func (c *Client) groupInfo(cmds []string) {
	if len(cmds) == 0 {
		return
	}

	groupIdList := []uint{}
	for _, id := range cmds {
		val, err := strconv.Atoi(id)
		if err == nil {
			groupIdList = append(groupIdList, uint(val))
		}
	}

	req := &proto.IMGroupInfoListReq{
		GroupIdList: groupIdList,
	}
	sendBodyBytes, _ := json.Marshal(req)
	sendPack := proto.NewMsg(proto.OpGroupInfoList, sendBodyBytes, 0)
	c.writeMsgChan <- sendPack
}

func (c *Client) groupChange(cmds []string, changeType int) {
	if len(cmds) == 0 {
		return
	}

	groupId, err := strconv.Atoi(cmds[0])
	if err != nil {
		fmt.Println("GroupAdd ERR:", err)
		return
	}

	memberIdList := []uint{}
	for _, id := range cmds[1:] {
		val, err := strconv.Atoi(id)
		if err == nil {
			memberIdList = append(memberIdList, uint(val))
		}
	}
	req := &proto.IMGroupChangeMemberReq{
		GroupId:      uint(groupId),
		UserId:       uint(c.Id),
		MemberIdList: memberIdList,
		ChangeType:   changeType,
	}
	sendBodyBytes, _ := json.Marshal(req)
	sendPack := proto.NewMsg(proto.OpGroupChangeMember, sendBodyBytes, 0)
	c.writeMsgChan <- sendPack
}

func (c *Client) createGroup(cmds []string) {
	if len(cmds) == 0 {
		return
	}

	groupName := cmds[0]
	memberList := []uint{uint(c.Id)}
	for _, id := range cmds[1:] {
		val, err := strconv.Atoi(id)
		if err == nil {
			memberList = append(memberList, uint(val))
		}
	}

	req := proto.IMGroupCreateReq{
		UserId:       uint(c.Id),
		GroupName:    groupName,
		MemberIdList: memberList,
	}
	sendBodyBytes, _ := json.Marshal(req)
	sendPack := proto.NewMsg(proto.OpGroupCreate, sendBodyBytes, 0)
	c.writeMsgChan <- sendPack
}

func (c *Client) Close() {
	if c.conn != nil {
		c.conn.Close()
	}
}

type FormAuthToken struct {
	AuthToken string `json:"authToken"`
	LastTime  int64  `json:"lastTime"`
}

type Response struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

func register(userName string, password string, nickname string) error {
	var data = &proto.IMRegister{
		UserName: userName,
		Password: password,
		NickName: nickname,
	}
	_body, _ := json.Marshal(data)
	body := bytes.NewReader(_body)
	rep, err := http.Post(apiAddr+registerPath, contentType, body)
	if err != nil {
		return err
	}

	rbyte, _ := ioutil.ReadAll(rep.Body)
	var r = &Response{}
	err = json.Unmarshal(rbyte, r)
	if err != nil {
		return err
	}
	if r.Code != proto.CodeSuccess {
		return errors.New(r.Message)
	}

	return nil
}

type ResponseUser struct {
	Code    int            `json:"code"`
	Message string         `json:"message"`
	Data    []proto.IMUser `json:"data"`
}

type ResponseLogin struct {
	Code    int              `json:"code"`
	Message string           `json:"message"`
	Data    proto.IMLoginRes `json:"data"`
}

func (c *Client) login(userName string, password string) error {
	var data = &form.FormLogin{
		UserName: userName,
		Password: password,
	}
	_body, _ := json.Marshal(data)
	body := bytes.NewReader(_body)
	rep, err := http.Post(apiAddr+loginPath, contentType, body)
	if err != nil {
		return err
	}
	rbyte, _ := ioutil.ReadAll(rep.Body)
	var r = &ResponseLogin{}
	err = json.Unmarshal(rbyte, r)
	if err != nil {
		return err
	}
	if r.Code != proto.CodeSuccess {
		return errors.New(r.Message)
	}

	c.authToken = r.Data.AuthToken
	c.Id = int(r.Data.UserInfo.Id)
	c.Name = r.Data.UserInfo.RealName
	return nil
}

func (c *Client) GetAllUser() {
	var req proto.IMAllUserReq
	req.LastTime = 0
	body, _ := json.Marshal(req)
	c.writeMsgChan <- proto.NewMsg(proto.OpAllUser, body, 0)
}

type ResponseOnline struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    []uint `json:"data"`
}

func (c *Client) GetOnlineUser() {
	c.writeMsgChan <- proto.NewMsg(proto.OpOnlineUser, nil, 0)
}

func (c *Client) GetAuthTokenReader() io.Reader {
	data := FormAuthToken{
		AuthToken: c.authToken,
		LastTime:  0,
	}

	_body, _ := json.Marshal(data)
	return bytes.NewReader(_body)
}

func (c *Client) queryMessage(cmds []string) {

	if len(cmds) < 2 {
		return
	}

	var beginId int
	var sessionId int
	var err error
	beginId, err = strconv.Atoi(cmds[0])
	if err != nil {
		return
	}
	sessionId, err = strconv.Atoi(cmds[1])
	if err != nil {
		return
	}

	req := &proto.IMGetMsgListReq{
		ThisUserId:  uint(c.Id),
		SessionId:   uint(sessionId),
		MsgIdBegin:  beginId,
		MsgCount:    10,
		SessionType: 0,
	}

	body, _ := json.Marshal(req)
	c.writeMsgChan <- proto.NewMsg(proto.OpGetMsgList, body, 0)
}

func (c *Client) queryLastMsg(cmds []string) {
	if len(cmds) < 1 {
		return
	}

	sessionId, err := strconv.Atoi(cmds[0])
	if err != nil {
		return
	}

	req := &proto.IMGetLastestMsgIdReq{
		UserId:      uint(c.Id),
		SessionId:   uint(sessionId),
		SessionType: 0,
	}

	body, _ := json.Marshal(req)
	c.writeMsgChan <- proto.NewMsg(proto.OpGetLatestMsgId, body, 0)
}
